package com.google.api.adwords.starterkit.Synchronizer;

import java.rmi.RemoteException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet;

import javax.xml.rpc.ServiceException;

import org.apache.log4j.Logger;

import com.google.api.adwords.starterkit.db.keys.NegativeKeywordKey;
import com.google.api.adwords.starterkit.db.structures.NegativeKeywordStructure;
import com.google.api.adwords.starterkit.db.tables.CampaignTable;
import com.google.api.adwords.starterkit.db.tables.NegativeKeywordTable;
import com.google.api.adwords.starterkit.settings.StarterKitSettings;
import com.google.api.adwords.starterkit.utils.AdWordsReportHandler;
import com.google.api.adwords.starterkit.utils.DumpExceptions;
import com.google.api.adwords.starterkit.utils.RowUtils;
import com.google.api.adwords.starterkit.utils.ServiceUtils;
import com.google.api.adwords.starterkit.utils.Utils;
import com.google.api.adwords.v201101.cm.ApiException;
import com.google.api.adwords.v201101.cm.CampaignCriterion;
import com.google.api.adwords.v201101.cm.CampaignCriterionOperation;
import com.google.api.adwords.v201101.cm.CampaignCriterionReturnValue;
import com.google.api.adwords.v201101.cm.CampaignCriterionServiceInterface;
import com.google.api.adwords.v201101.cm.Keyword;
import com.google.api.adwords.v201101.cm.KeywordMatchType;
import com.google.api.adwords.v201101.cm.NegativeCampaignCriterion;
import com.google.api.adwords.v201101.cm.Operator;
import com.google.api.adwords.v201101.cm.Predicate;
import com.google.api.adwords.v201101.cm.PredicateOperator;
import com.google.api.adwords.v201101.cm.ReportDefinitionReportType;

/**
 * The Class NegativeKeywordsSynchronizer is responsible to keep all 
 * NegativeKeywords information from AdWords system synchronized with local DB.
 * 
 * @author rodrigo.f.vale@gmail.com (Rodrigo Vale)
 */
public class NegativeKeywordsSynchronizer {
  private NegativeKeywordTable negativeKeywordTable;
  private CampaignTable campaignTable;
  private ServiceUtils serviceUtils;
  private StarterKitSettings starterKitSetting;
  private static final Logger log = Logger.getLogger("StarterKit");

  public NegativeKeywordsSynchronizer(StarterKitSettings starterKitSetting, 
      Connection conn) {
    setServiceUtils(new ServiceUtils(starterKitSetting));
    this.starterKitSetting = starterKitSetting;

    campaignTable = new CampaignTable(conn);
    negativeKeywordTable = new NegativeKeywordTable(conn);
  }
  
  /**
   * Gets the Campaign Negative Keywords Performance report.
   * 
   * @return the Campaign Negative Keywords Performance report handler
   */
  public AdWordsReportHandler getReportHandlerPerformance() {
    log.debug("Defining Campaign Negative Keywords Performance Report");
  
    String fields[] = new String[] { "CampaignId", "Id", "KeywordMatchType",
        "KeywordText" };
  
    String fileName = starterKitSetting.getCacheDir().concat(
        "CampaignNegativeKeywordsPerformance.xml");
  
    HashMap<Long, Integer> campaignsMapsIds = campaignTable
        .getCampaignId2LocalId();
  
    Long[] campaignsIds = campaignsMapsIds.keySet().toArray(new Long[0]);
    String[] sCampaignsIds = new String[campaignsIds.length];
    
    Predicate[] predicates = null;
    if (campaignsIds.length > 0) {
      for (int i = 0; i < campaignsIds.length; i++) {
        sCampaignsIds[i] = campaignsIds[i].toString();
      }

      // Create campaing predicate.
      Predicate campaingPredicate = new Predicate();
      campaingPredicate.setField("CampaignId");
      campaingPredicate.setOperator(PredicateOperator.IN);
      campaingPredicate.setValues(sCampaignsIds);

      predicates = new Predicate[] { campaingPredicate };
    }
    AdWordsReportHandler adWordsReportHandler = new AdWordsReportHandler(
        getServiceUtils(), starterKitSetting);
    adWordsReportHandler
        .requestAdWordsReport(
            ReportDefinitionReportType
            .CAMPAIGN_NEGATIVE_KEYWORDS_PERFORMANCE_REPORT,
            fields, fileName, predicates);
  
    return adWordsReportHandler;
  }

  /**
   * Load negative keywords from adwords.
   */
  public void get() {
    log.debug("Start to load NegativeKeywords");
  
    negativeKeywordTable.resetIsBanned(false);
  
    AdWordsReportHandler adWordsReportHandler 
        = getReportHandlerPerformance();
  
    HashMap<Long, Integer> campaignsIds = campaignTable.getCampaignId2LocalId();
  
    log.debug("Processing NegativeKeywords JSON Objects");
    int count = 0;
    while (adWordsReportHandler.hasNext()) {
      HashMap<String, String> row = adWordsReportHandler.next();
      processReportData(row, campaignsIds);
      count++;
      if (count == 100000) {
        count = 0;
        negativeKeywordTable.execBatch();
      }
    }
    negativeKeywordTable.execBatch();
    negativeKeywordTable.resetStatus();
  }

  /**
   * Process an AdWords report row and add it to a batch update into the
   * negative keyword table.
   * 
   * @param row
   *          The row from the Adwords Report.
   * @param campaignsIds
   *          The map between localId and campaignId at campaign table.
   */
  protected void processReportData(
      HashMap<String, String> row, HashMap<Long, Integer> campaignsIds) {
    NegativeKeywordStructure negKwdStruct = new NegativeKeywordStructure();
    negKwdStruct.setCampaignId(RowUtils.getLong(row, "campaignID", -1));
    negKwdStruct.setKeywordId(RowUtils.getLong(row, "keywordID", -1));
    negKwdStruct.setMatchType(KeywordMatchType.fromString(RowUtils.getString(
        row, "matchType", "").toUpperCase()));
    negKwdStruct.setText(RowUtils.getString(row, "negativeKeyword", ""));
  
    NegativeKeywordKey negKwdKey = new NegativeKeywordKey(campaignsIds
        .get(negKwdStruct.getCampaignId()), negKwdStruct.getMatchType(),
        negKwdStruct.getText());
  
    NegativeKeywordStructure nKeywordStructureDB = negativeKeywordTable
        .get(negKwdKey);
  
    if (nKeywordStructureDB == null) {
      negKwdStruct.setLocalCampaignId(negKwdKey.getLocalCampaignId());
      negKwdStruct.setMatchModifiable(false);
      negKwdStruct.setBanned(false);
      negativeKeywordTable.addToBatch(negKwdStruct, false, false);
    } else {
      nKeywordStructureDB.setBanned(false);
      negativeKeywordTable.addToBatch(nKeywordStructureDB, false, false);
    }
  }

  /**
   * Process updates in negative campaign keywords table.
   */
  protected void put() {
    log.debug("Preparing Negative Campaign Keywords to be sent to AdWords");
    negativeKeywordTable.updateCampaignsIds();
  
    HashSet<CampaignCriterionOperation> operations 
        = new HashSet<CampaignCriterionOperation>();
  
    try {
      // add or update new negative keywords
      ResultSet rs = negativeKeywordTable.getModified();
      while (rs.next()) {
        NegativeKeywordStructure negKwdStruct = negativeKeywordTable
            .getStructure(rs);
  
        Keyword keyword = Utils.createKeyword(negKwdStruct);
  
        CampaignCriterionOperation campCritOperation 
            = new CampaignCriterionOperation();
        NegativeCampaignCriterion campCriterion = new NegativeCampaignCriterion(
            negKwdStruct.getCampaignId(), keyword, null);
        campCritOperation.setOperand(campCriterion);
        if (negKwdStruct.getKeywordId() == 0) {
          campCritOperation.setOperator(Operator.ADD);
        } else {
          campCritOperation.setOperator(Operator.SET);
        }
        operations.add(campCritOperation);
      }
      rs.close();
  
      // delete negative keywords
      if (starterKitSetting.getCampaignSettings().getAutoPause()) {
        rs = negativeKeywordTable.getUnTouched();
        if (rs != null) {
          while (rs.next()) {
            NegativeKeywordStructure negKwdStruct = negativeKeywordTable
                .getStructure(rs);
  
            Keyword keyword = Utils.createKeyword(negKwdStruct);
  
            CampaignCriterionOperation campCritOperation 
                = new CampaignCriterionOperation();
            NegativeCampaignCriterion campCriterion 
                = new NegativeCampaignCriterion(
                negKwdStruct.getCampaignId(), keyword, null);
            campCritOperation.setOperand(campCriterion);
            if (negKwdStruct.getLocalId() != 0) {
              campCritOperation.setOperator(Operator.REMOVE);
            }
            operations.add(campCritOperation);
          }
          rs.close();
        }
      }
      processOperations(operations);
    } catch (SQLException e) {
      DumpExceptions.dumpSQLException(e);
    }
    negativeKeywordTable.resetStatus();
  }

  /**
   * Saves NegativeCampaignKeywords processed updates to AdWords.
   * 
   * @param operations
   *          The updates to be sent to AdWords
   */
  protected void processOperations(
      HashSet<CampaignCriterionOperation> operations) {
    if (operations.size() > 0) {
      CampaignCriterionOperation[] op = operations
          .toArray(new CampaignCriterionOperation[operations.size()]);
      try {
        CampaignCriterionServiceInterface service = getServiceUtils()
            .getCampaignCriterionService();
        CampaignCriterionReturnValue result = service.mutate(op);
  
        CampaignCriterion[] criterions = result.getValue();
        for (CampaignCriterion criterion : criterions) {
            Keyword keyword = (Keyword) criterion.getCriterion();            
            negativeKeywordTable.updateKeywordId(
                keyword.getId(), 
                criterion.getCampaignId(), 
                keyword.getText(), 
                keyword.getMatchType());
        }
        log.debug("Updated " + op.length + " KegativeCampaigns");
      } catch (ApiException e) {
        DumpExceptions.dumpApiException(e);
      } catch (RemoteException e) {
        DumpExceptions.dumpRemoteException(e);
      } catch (ServiceException e) {
        DumpExceptions.dumpServiceException(e);
      }
    }
  }

  public ServiceUtils getServiceUtils() {
    return serviceUtils;
  }

  public void setServiceUtils(ServiceUtils serviceUtils) {
    this.serviceUtils = serviceUtils;
  }
}
